/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package aimozg.joglfw;

import aimozg.joglfw.gui.GLBlock;
import com.sun.opengl.util.Animator;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.GLUT;
import com.sun.opengl.util.j2d.TextRenderer;
import java.awt.Font;
import java.awt.Insets;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;
import java.awt.font.LineMetrics;
import javax.media.opengl.GL;
import javax.media.opengl.GLCapabilities;
import static javax.media.opengl.GL.*;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import javax.swing.JFrame;

/**
 *
 * @author aimozg
 */
public abstract class JoglApplication {
	public static enum PrimitiveType{
		POINTS(GL_POINTS),
		LINES(GL_LINES),
		LINE_LOOP(GL_LINE_LOOP),
		LINE_STEIP(GL_LINE_STRIP),
		TRIANGLES(GL_TRIANGLES),
		TRIANGLE_STRIP(GL_TRIANGLE_STRIP),
		TRIANGLE_FAN(GL_TRIANGLE_FAN),
		QUADS(GL_QUADS),
		QUAD_STRIP(GL_QUAD_STRIP),
		POLYGON(GL_POLYGON);
		public final int glConstant;

		private PrimitiveType(int glConstant) {
			this.glConstant = glConstant;
		}
	}

	private long dt = 0;
	private long time1;
	private double framesCount;
	private long time0;
	private long timePrev;
	private long timerStart = 0;

	protected long dt(){
		return dt;
	}

	protected long timerTime(){
		return System.currentTimeMillis()-timerStart;
	}

	protected void timerReset(){
		timerStart = System.currentTimeMillis();
	}

	////////////////////////////////////////////////////////////////////////////
    // OVERRIDEABLE OPTIONS AND EVENT HANDLERS
    ////////////////////////////////////////////////////////////////////////////
    protected void windowClosing(WindowEvent e) {
        stop();
    }

    public GL gl() {
        return gl;
    }

    public static class Options {

        public boolean defaultInit = true;
        public boolean resizeOnReshape = true;
        public boolean clearDisplay = true;
        public String windowTitle = "JOGL Application";
        public int windowWidth = 640;
        public int windowHeight = 480;
        public enum Projection {ORTHO, PERSPECTIVE};
        public Projection projection = Projection.ORTHO;
        public double perspectiveFOV = 90.0;
        public double perspectiveZNear = 1.0;
        public double perspectiveZFar = 1000.0;
        public boolean renderRootBefore = true;
        public boolean renderRootAfter = false;
		public long frameSleep = 1000/60;
        private Options(){}
    }
    protected final Options options = new Options();
    protected GL gl;
    protected GLAutoDrawable drawable;
    protected int currentWidth;
    protected int currentHeight;

    public int height() {
        return currentHeight;
    }

    public int width() {
        return currentWidth;
    }
    
    

    protected void customInit() {
    }

    protected void customDisplayChanged(boolean modeChanged, boolean deviceChanged) {
    }

    protected void setupScreen(int x, int y, int width, int height) {
        currentWidth = width;
        currentHeight = height;
        if (options.projection == Options.Projection.ORTHO){
            gl.glOrtho(0, width, height, 0, -1.0, 1.0);
        } else {
            double aspect = ((double)width)/height;
            new GLU().gluPerspective(options.perspectiveFOV/aspect, aspect, options.perspectiveZNear, options.perspectiveZFar);
        }
    }

    protected abstract void render();

    protected void mouseClicked(MouseEvent e) {
    }

    protected void mousePressed(MouseEvent e) {
    }

    protected void mouseReleased(MouseEvent e) {
    }

    protected void mouseEntered(MouseEvent e) {
    }

    protected void mouseExited(MouseEvent e) {
    }

    protected void mouseDragged(MouseEvent e) {
    }

    protected void mouseMoved(MouseEvent e) {
    }

    protected void keyTyped(KeyEvent e) {
    }

    protected void keyPressed(KeyEvent e) {
    }

    protected void keyReleased(KeyEvent e) {
    }

    ////////////////////////////////////////////////////////////////////////////
    // PRIVATE AND PUBLIC OPENGL AND FRAME LOGIC
    ////////////////////////////////////////////////////////////////////////////
    private void init(GLAutoDrawable drawable) {
        this.drawable = drawable;
        gl = drawable.getGL();
        if (options.defaultInit) {
            gl.glShadeModel(GL_SMOOTH);
            gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            gl.glClearDepth(1.0f);
            gl.glEnable(GL_DEPTH_TEST);
            gl.glEnable(GL_ALPHA_TEST);
	        gl.glEnable(GL_BLEND);
	        gl.glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
            gl.glDepthFunc(GL_LEQUAL);
            gl.glAlphaFunc(GL_GEQUAL, 0.1f);
            gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT,
                    GL_NICEST);
        }
        customInit();
        drawable.addKeyListener(new KeyListener() {

            public void keyTyped(KeyEvent e) {
                JoglApplication.this.keyTyped(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.keyboardEvent(e);
            }

            public void keyPressed(KeyEvent e) {
                JoglApplication.this.keyPressed(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.keyboardEvent(e);
            }

            public void keyReleased(KeyEvent e) {
                JoglApplication.this.keyReleased(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.keyboardEvent(e);
            }
        });
        drawable.addMouseListener(new MouseListener() {

            public void mouseClicked(MouseEvent e) {
                JoglApplication.this.mouseClicked(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }

            public void mousePressed(MouseEvent e) {
                JoglApplication.this.mousePressed(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }

            public void mouseReleased(MouseEvent e) {
                JoglApplication.this.mouseReleased(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }

            public void mouseEntered(MouseEvent e) {
                JoglApplication.this.mouseEntered(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }

            public void mouseExited(MouseEvent e) {
                JoglApplication.this.mouseExited(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }
        });
        drawable.addMouseMotionListener(new MouseMotionListener() {

            public void mouseDragged(MouseEvent e) {
                JoglApplication.this.mouseDragged(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }

            public void mouseMoved(MouseEvent e) {
                JoglApplication.this.mouseMoved(e);
                if (rootPanel != null && !e.isConsumed()) rootPanel.mouseEvent(e);
            }
        });
    }

    private void renderRootPanel(){
        gl.glEnable(GL_STENCIL_TEST);
        gl.glClearStencil(0);
        rootPanel.render();
        gl.glDisable(GL_STENCIL_TEST);
    }

    private void display(GLAutoDrawable drawable) {
        this.drawable = drawable;
        gl = drawable.getGL();
        if (options.clearDisplay) {
            gl.glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
            gl.glMatrixMode(GL_MODELVIEW);
            gl.glLoadIdentity();
        }
        if (rootPanel!=null && options.renderRootBefore){
            renderRootPanel();
        }
        render();
        if (rootPanel!=null && options.renderRootAfter){
            renderRootPanel();
        }
	    //////////
	    if (options.frameSleep>0) try{
		    Thread.sleep(options.frameSleep );
	    } catch (InterruptedException ex){}
	    time1 = System.currentTimeMillis();
		dt=0;
	    framesCount++;
		dt = time1-timePrev;
		timePrev = time1;
	    if (time1-time0 > 500) {
	        FPS = framesCount * 1000 / (time1-time0);
	        time0 = time1;
	        framesCount = 0;
	    }
    }

    private void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        this.drawable = drawable;
        gl = drawable.getGL();
	    gl.glMatrixMode(GL_PROJECTION);
	    gl.glLoadIdentity();
	    if (options.resizeOnReshape) {
	        setupScreen(x, y, width, height);
	    } else {
	        setupScreen(x, y, options.windowWidth, options.windowHeight);
	    }
	    gl.glMatrixMode(GL_MODELVIEW);
	    gl.glLoadIdentity();
    }

    private void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
        this.drawable = drawable;
        gl = drawable.getGL();
        customDisplayChanged(modeChanged, deviceChanged);
    }
	final Object bQuitLock = new Object();

    protected void initComponents(){
        frame.add(canvas);
        frame.setSize(options.windowWidth,options.windowHeight);
        frame.setVisible(true);
        Insets insets = frame.getInsets();
        frame.setSize(options.windowWidth+insets.left+insets.right, 
                options.windowHeight+insets.top+insets.bottom);
    }

	private Animator animator;

    public void launch() {
        if (running) {
            throw new IllegalStateException("JoglApplication already running");
        }
        running = true;
	    time1 = time0 = timerStart = System.currentTimeMillis();
	    framesCount = 0;
	    timePrev = time0;
        frame = new JFrame(options.windowTitle);
        GLCapabilities caps = new GLCapabilities();
        caps.setDoubleBuffered(true);
        caps.setStencilBits(1);
        canvas = new GLCanvas(caps);
        canvas.addGLEventListener(new GLEventListener() {

            public void init(GLAutoDrawable drawable) {
                JoglApplication.this.init(drawable);
            }

            public void display(GLAutoDrawable drawable) {
                JoglApplication.this.display(drawable);
            }

            public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
                JoglApplication.this.reshape(drawable, x, y, width, height);
            }

            public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
                JoglApplication.this.displayChanged(drawable, modeChanged, deviceChanged);
            }
        });
        initComponents();
        frame.addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                JoglApplication.this.windowClosing(e);
            }
        });
        frame.setVisible(true);
        canvas.requestFocus();
	    animator = new Animator(canvas);
	    animator.setRunAsFastAsPossible(false);
	    animator.start();
        while (!bQuit) {
	        synchronized (bQuitLock){
				try{
					bQuitLock.wait(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();  //TODO handle exception
				}
	        }
	        canvas.display();
        }

    }

    public void stop() {
	    synchronized (bQuitLock){
            bQuit = true;
		    bQuitLock.notifyAll();
	    }
    }
    private boolean running = false;
    private volatile boolean bQuit = false;
    ////////////////////////////////////////////////////////////////////////////
    // SOME USEFUL METHODS AND FIELDS; SHORTHANDS FOR gl.glXXXX
    protected JFrame frame;
    protected GLCanvas canvas;
    protected double FPS = Double.NaN;
    protected GLUT glut = new GLUT();

    public static GLPoint Pt(double x, double y, double z) {
        return new GLPoint(x, y, z);
    }

    public static GLPoint Pt(double x, double y) {
        return new GLPoint(x, y);
    }

	public static GLTexCoord Tx(double x,double y){
		return new GLTexCoord(x,y);
	}

    public static GLColor Cl(double red, double green, double blue, double alpha) {
        return new GLColor(red, green, blue, alpha);
    }

    public static GLColor Cl(double red, double green, double blue) {
        return new GLColor(red, green, blue);
    }

	public void begin(PrimitiveType type){
		gl.glBegin(type.glConstant);
	}

	public void renderItems(PrimitiveType type, GLRenderable... pts){
		renderItems(type.glConstant, pts);
	}

    private void renderItems(int glMode, GLRenderable[] pts) {
        gl.glBegin(glMode);
        renderItems(pts);
        gl.glEnd();
    }

    public final void drawPoints(GLRenderable... pts) {
        renderItems(GL_POINTS, pts);
    }

    public final void beginPoints() {
        gl.glBegin(GL_POINTS);
    }

    public final void drawLines(GLRenderable... pts){
        renderItems(GL_LINES, pts);
    }

    public final void beginLines() {
        gl.glBegin(GL_LINES);
    }

    public final void drawLineLoop(GLRenderable... pts) {
        renderItems(GL_LINE_LOOP, pts);
    }

    public final void beginLineLoop() {
        gl.glBegin(GL_LINE_LOOP);
    }

    public final void drawLineStrip(GLRenderable... pts) {
        renderItems(GL_LINE_STRIP, pts);
    }

    public final void beginLineStrip() {
        gl.glBegin(GL_LINE_STRIP);
    }

    public final void drawTriangles(GLRenderable... pts) {
        renderItems(GL_TRIANGLES, pts);
    }

    public final void beginTriangles() {
        gl.glBegin(GL_TRIANGLES);
    }

    public final void drawTriangleStrip(GLRenderable... pts) {
        renderItems(GL_TRIANGLE_STRIP, pts);
    }

    public final void beginTriangleStrip() {
        gl.glBegin(GL_TRIANGLE_STRIP);
    }

    public final void drawQuads(GLRenderable... pts) {
        renderItems(GL_QUADS, pts);
    }

    public final void beginQuads() {
        gl.glBegin(GL_QUADS);
    }

    public final void end() {
        gl.glEnd();
    }

    public final void setColor(GLColor c) {
        c.render(gl);
    }

    public final void setColor(double red, double green, double blue) {
        gl.glColor3d(red, green, blue);
    }

    public final void setColor(double red, double green, double blue, double alpha) {
        gl.glColor4d(red, green, blue, alpha);
    }

	public final void renderItems(GLRenderable... renderables){
		for (GLRenderable r:renderables){
			r.render(gl());
		}
	}

    public final void point(GLPoint pt) {
        pt.render(gl);
    }

    public final void point(double x, double y) {
        gl.glVertex2d(x, y);
    }

    public final void point(double x, double y, double z) {
        gl.glVertex3d(x, y, z);
    }

    public final void rasterGoto(double x, double y) {
        gl.glRasterPos2d(x, y);
    }
    private int glutBitmapFont = GLUT.BITMAP_8_BY_13;

    public enum BitmapFont {
        B8x13(GLUT.BITMAP_8_BY_13),
        B9x15(GLUT.BITMAP_9_BY_15),
        HELVETICA10(GLUT.BITMAP_HELVETICA_10),
        HELVETICA12(GLUT.BITMAP_HELVETICA_12),
        HELVETICA18(GLUT.BITMAP_HELVETICA_18),
        TIMES10(GLUT.BITMAP_TIMES_ROMAN_10),
        TIMES24(GLUT.BITMAP_TIMES_ROMAN_24);

        public final int code;

        private BitmapFont(int code) {
            this.code = code;
        }

    }

    public final void setBitmapFont(BitmapFont font) {
        glutBitmapFont = font.code;
    }

    public final void bitmapText(String text) {
        glut.glutBitmapString(glutBitmapFont, text);
    }

    public final void bitmapText(double x, double y, char text){
        rasterGoto(x, y);
        glut.glutBitmapCharacter(glutBitmapFont, text);
    }
    
    public final void bitmapText(double x, double y, String text) {
        rasterGoto(x, y);
        bitmapText(text);
    }

    public final void bitmapText(BitmapFont font, double x, double y, String text) {
        setBitmapFont(font);
        rasterGoto(x, y);
        bitmapText(text);
    }

    /**
     * Обёртка для произвольного шрифта
     */
    public static class GLFont {

        private Font awtFont;
        private TextRenderer trr;
        private double height;
        private double baseline;

	    private double xscale = 1.0;
	    private double yscale = 1.0;
	    private double zscale = 1.0;

        public Font getAwtFont() {
            return awtFont;
        }

	    public double getXscale() {
		    return xscale;
	    }

	    public void setXscale(double xscale) {
		    this.xscale = xscale;
	    }

	    public double getYscale() {
		    return yscale;
	    }

	    public void setYscale(double yscale) {
		    this.yscale = yscale;
	    }

	    public double getZscale() {
		    return zscale;
	    }

	    public void setZscale(double zscale) {
		    this.zscale = zscale;
	    }

	    public GLFont(Font awtFont) {
            this.awtFont = awtFont;
            trr = new TextRenderer(awtFont);
            //height = trr.getBounds("Wg").getHeight();
            LineMetrics lm = awtFont.getLineMetrics("", trr.getFontRenderContext());
            //height = awtFont.getMaxCharBounds(trr.getFontRenderContext()).getHeight();
            height = lm.getHeight();
            baseline = lm.getDescent();
        }
        
        public GLFont(String fontName,int fontSize) {
        	this(new Font(fontName, 0, fontSize));
        }
        public GLFont(String fontName,int fontSize, boolean bold, boolean italic) {
        	this(new Font(fontName,
        			(bold?Font.BOLD:0) |
        			(italic?Font.ITALIC:0),
        			fontSize));
        }

        private void renderText(JoglApplication app, String text, GLColor color,double x, double y) {
            trr.begin3DRendering();
	        if (color != null){
                trr.setColor((float)color.red,(float)color.green,(float)color.blue,(float)color.alpha);
	        }
            //Пляски с бубнами, вызванные различием в системах координат
            {
                app.pushMatrix();
                {
                    app.translate(0, 2 * y, 0);//Подгоночный коэффициент
                    app.scale(xscale, -yscale, zscale);
                    trr.draw3D(text, (float) x, (float) (y+baseline-height+1), 0f, 1f);
                    trr.flush();
                }
                app.popMatrix();
            }
            trr.endRendering();
            app.gl.glMatrixMode(GL_MODELVIEW);
        }

        public double getHeight() {
            return height;
        }
    }
    /**
     * Отрендерить строку <code>text</code> в точке <code>x,y</code> шрифтом
     * <code>font</code>
     */
    public final void renderText(GLFont font, String text, GLColor color,double x, double y) {
        font.renderText(this,text,color, x, y);
    }
    public final void renderText(GLFont font, char text, GLColor color,double x, double y) {
        font.renderText(this,String.valueOf(text),color, x, y);
    }
    /**
     * Сохранение матрицу в стек
     */
    public final void pushMatrix(){
        gl.glPushMatrix();
    }
    /**
     * Загрузка матрицы из стека
     */
    public final void popMatrix(){
        gl.glPopMatrix();
    }
    /**
     * Масштабирование в <code>x,y,z</code> раз по соответствующим осям
     */
    public final void scale(double x,double y,double z){
        gl.glScaled(x,y,z);
    }
    /**
     * Сдвиг на <code>x,y,z</code>
     */
    public final void translate(double x, double y, double z){
        gl.glTranslated(x,y,z);
    }
    /**
     * Вращение на угол <code>angle</code> вокруг вектора <code>x,y,z</code>
     */
    public final void rotate(double angle, double x, double y, double z){
        gl.glRotated(angle, x, y, z);
    }
    /**
     * Вращение на угол <code>angle</code> вокруг вектора <code>vx,vy,vz</code>,
     * исходящего из точки <code>px,py,pz</code>
     */
    public final void rotateAround(double px, double py, double pz, double angle, double vx, double vy, double vz){
        translate(px,py,pz);
        rotate(angle,vx,vy,vz);
        translate(-px,-py,-pz);
    }

    protected GLBlock rootPanel = new GLBlock(this);

    public JoglApplication() {
        rootPanel.setWidth(options.windowWidth);
        rootPanel.setHeight(options.windowHeight);
        rootPanel.setBorderColor(Cl(0,0,0,0));
        rootPanel.setBgColor(Cl(0,0,0,0));
        rootPanel.setTextColor(Cl(0,0,0,0));
    }


}
